package cn.lingyangwl.agile.auth.controller;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.lingyangwl.agile.auth.assembly.OAuth2OpenAssembly;
import cn.lingyangwl.agile.model.module.auth.GrantTypeEnum;
import cn.lingyangwl.agile.auth.oauth2.model.OAuth2CheckTokenResp;
import cn.lingyangwl.agile.auth.oauth2.model.OAuth2TokenResp;
import cn.lingyangwl.agile.auth.oauth2.model.RegisteredClient;
import cn.lingyangwl.agile.auth.oauth2.support.BaseOauth2Grant;
import cn.lingyangwl.agile.auth.service.OAuth2ClientService;
import cn.lingyangwl.agile.auth.service.OAuth2GrantService;
import cn.lingyangwl.agile.auth.service.OAuth2TokenService;
import cn.lingyangwl.agile.auth.utils.OAuth2Utils;
import cn.lingyangwl.agile.model.enums.AuthErrorEnum;
import cn.lingyangwl.agile.model.module.auth.OAuth2ParamKeys;
import cn.lingyangwl.framework.core.response.Resp;
import cn.lingyangwl.framework.tool.core.exception.BizException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;

/**
 * 提供给外部应用调用为主, 管理后台也可以用, 但是不接入第三方登录(微信/qq/企业微信), 这些属于另外的概念
 *
 * 一般来说，管理后台的接口是不直接提供给外部应用使用, 但是可以通过给client授予一个角色, 来控制权限供外部使用(情况比较少)
 *
 * 参考大量的开放平台，都是独立的一套 OpenAPI，对应到【本系统】就是在 Controller 下新建 open 包，实现 /open-api/* 接口，
 * 然后通过 scope 进行控制。
 *
 * 考虑到【本系统】暂时不想做的过于复杂，默认只有获取到 access token 之后，可以访问【本系统】管理后台的所有接口，除非手动添加 scope 控制。
 * scope 的使用示例，可见 {@link OAuth2UserController} 类
 *
 * @author shenguangayng
 */
@Tag(name = " OAuth2.0 授权")
@RestController
@RequestMapping("/oauth2")
@Validated
@Slf4j
public class OAuth2OpenController {
    @Resource
    private OAuth2ClientService clientService;
    @Resource
    private OAuth2GrantService oauth2GrantService;
    @Resource
    private OAuth2TokenService tokenService;
    @Resource
    private OAuth2OpenAssembly oAuth2OpenAssembly;

    /**
     * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法
     *
     * 授权码 authorization_code 模式时：code + redirectUri + state 参数
     * 密码 password 模式时：username + password + scope 参数
     * 刷新 refresh_token 模式时：refreshToken 参数
     * 客户端 client_credentials 模式：scope 参数
     * 简化 implicit 模式时：不支持
     *
     * 注意，默认需要传递 client_id + client_secret 参数
     * @see OAuth2ParamKeys
     */
    @PostMapping("/token")
    @PermitAll
    @Operation(summary = "获得访问令牌", description = "适合 code 授权码模式，或者 implicit 简化模式；在 sso.vue 单点登录界面被【获取】调用")
    @Parameters({
            @Parameter(name = "grant_type", required = true, description = "授权类型", example = "code"),
            @Parameter(name = "code", description = "授权范围", example = "userinfo.read"),
            @Parameter(name = "redirect_uri", description = "重定向 URI", example = "https://www.iocoder.cn"),
            @Parameter(name = "state", description = "状态", example = "1"),
            @Parameter(name = "username", example = "tudou"),
            @Parameter(name = "password", example = "cai"), // 多个使用空格分隔
            @Parameter(name = "scope", example = "user_info"),
            @Parameter(name = "refresh_token", example = "123424233"),
    })
    public Resp<OAuth2TokenResp> postAccessToken(HttpServletRequest request) {
        MultiValueMap<String, String> parameters = OAuth2Utils.getParameters(request);
        String grantType = parameters.getFirst(OAuth2ParamKeys.GRANT_TYPE);

        // 1.1 校验授权类型
        GrantTypeEnum grantTypeEnum = GrantTypeEnum.of(grantType);
        if (grantTypeEnum == null) {
            throw new BizException(AuthErrorEnum.OAUTH2_GRANT_TYPE_NOT_EXISTS, grantType);
        }
        if (grantTypeEnum == GrantTypeEnum.IMPLICIT) {
            throw new BizException( "Token 接口不支持 implicit 授权模式");
        }

        // 2. 根据授权模式，获取访问令牌
        OAuth2TokenResp resp = BaseOauth2Grant.grant(grantTypeEnum, request);
        return Resp.ok(resp);
    }


    @DeleteMapping("/token")
    @PermitAll
    @Operation(summary = "删除访问令牌")
    @Parameter(name = "token", required = true, description = "访问令牌")
    public Resp<Boolean> revokeToken(HttpServletRequest request,
                                     @RequestParam("token") String token) {
        // 校验客户端
        String[] clientIdAndSecret = obtainBasicAuthorization(request);
        RegisteredClient registeredClient = clientService.validOAuthClient(clientIdAndSecret[0], clientIdAndSecret[1],
                null, null, null);

        // 删除访问令牌
        return Resp.ok(oauth2GrantService.revokeToken(registeredClient.getClientId(), token));
    }

    /**
     * 对应 Spring Security OAuth 的 CheckTokenEndpoint 类的 checkToken 方法
     */
    @PostMapping("/check-token")
    @Operation(summary = "校验访问令牌")
    @Parameter(name = "token", required = true, description = "访问令牌")
    public Resp<OAuth2CheckTokenResp> checkToken(HttpServletRequest request,
                                                 @RequestParam("token") String token) {
        // 校验客户端
        String[] clientIdAndSecret = obtainBasicAuthorization(request);
        clientService.validOAuthClient(clientIdAndSecret[0], clientIdAndSecret[1],
                null, null, null);

        // 校验令牌
        OAuth2TokenResp tokenResp = tokenService.checkAccessToken(token);
        Assert.notNull(tokenResp, "访问令牌不能为空");
        return Resp.ok(oAuth2OpenAssembly.toOAuth2CheckTokenResp(tokenResp));
    }


    /**
     * 刷新访问令牌
     */
    @PostMapping("/refresh-token")
    @PermitAll
    @Operation(summary = "刷新令牌")
    @Parameter(name = "refreshToken", description = "刷新令牌", required = true)
    public Resp<OAuth2TokenResp> refreshToken(@RequestParam("refreshToken") String refreshToken) {
        return Resp.ok(tokenService.refreshAccessToken(refreshToken));
    }

    private String[] obtainBasicAuthorization(HttpServletRequest request) {
        String[] clientIdAndSecret = OAuth2Utils.obtainBasicAuthorization(request);
        if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) {
            throw new BizException("client_id 或 client_secret 未正确传递");
        }
        return clientIdAndSecret;
    }
}
